/*
    This file is part of QTau
    Copyright (C) 2013-2018  Tobias "Tomoko" Platen <tplaten@posteo.de>
    Copyright (C) 2013       digited       <https://github.com/digited>
    Copyright (C) 2010-2013  HAL@ShurabaP  <https://github.com/haruneko>

    QTau is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.

    SPDX-License-Identifier: GPL-3.0+
*/

#include "mainwindow.h"

#include <QJsonDocument>
#include "Controller.h"
#include "PluginInterfaces.h"
#include "Session.h"
#include "Utils.h"

#include "audio/audioengine.h"
#include "audio/jackaudio.h"

#include <assert.h>
#include <qsettings.h>
#include <score.h>
#include <tempomap.h>
#include <QApplication>
#include <QDebug>
#include <QDirIterator>
#include <QPluginLoader>
#include <QTimer>
#include <QProcess>
#include "archivedownloader.h"
#include <QMessageBox>

static QString defa = "https://archive.org/download/defa-vault-cv-japanese.tar/Defa%20Vault%20CV%20Japanese.tar.bz2";

#define __devloglevel__ 4

qtauController::qtauController(QString dir, QObject *parent)
    : QObject(parent),
      _jack(nullptr),
      _mainWindow(nullptr),
      _activeSession(nullptr) {
    QDir diru(dir);
    diru.cdUp();
    _prefix = diru.absolutePath();

    QSettings settings("QTau", c_qtau_name);
    bool autoconnect = settings.value("jack_auto_connect",true).toBool();

    _jack = new JackAudio(autoconnect);
    _audio = new AudioEngine(_jack, this);
    _audio->setUseJackTransport(true);
    _outbuf = new OutputBuffer(_jack);
    _audio->setOutputBuffer(_outbuf);
    _jackSampleRate = _jack->getSamplerate();

    connect(_outbuf, &OutputBuffer::startPlayback, this,
            &qtauController::outbuf_startPlayback);
    connect(_outbuf, &OutputBuffer::stopPlayback, this,
            &qtauController::outbuf_stopPlayback);

    QTimer *timer = new QTimer(this);
    connect(timer, SIGNAL(timeout()), this, SLOT(jackTimer()));
    timer->start(10);

    //_activeSynth = nullptr;
    //_selectedSynth = nullptr;

    setupPlugins();
    if(setupVoicebanks()==false)
    {
        QString userpath = QDir::home().filePath(".local/share/utau/voice");
        _needDownload = true;
        ArchiveDownloader* dl = new ArchiveDownloader(defa,userpath,this);
        connect(dl,&ArchiveDownloader::done,this,&qtauController::rescanVoiceDirectory);
        //FIXME: store dl -> delete after download done
    }
}
int qtauController::sampleRate() {
    return _jack->sampleRate();
}

void qtauController::startOfflinePlayback(const QString &fileName) {
    _outbuf->openReadFile(fileName);
    _lastPlay = fileName;
    startPlayback(0);
}

void qtauController::jackTimer() {
    char midi[1024];
    if (_jack->readMidiData(midi, 1024)) {
        int event_type = midi[0] & 0xF0;
        int note_num = midi[1];
        if (event_type == 144) {
            pianoKeyPressed(note_num);
        } else if (event_type == 128) {
            pianoKeyReleased(note_num);
        }
    }

    if (_jack->stateChanged()) {
        switch (_jack->transportState()) {
        case JackTransportStopped:
            DEVLOG_DEBUG("state_changed to JackTransportStopped");
            // XXX if(_activeSynth) _activeSynth->stopThread();
            transportPositionChanged(-1);
            break;
        case JackTransportStarting:
            DEVLOG_DEBUG("state_changed to JackTransportStarting");
            break;
        case JackTransportLooping:
            DEVLOG_DEBUG("not supported JackTransportLooping");
            break;
#if 0
        case JackTransportNetStarting:
            DEVLOG_DEBUG("not supported JackTransportNetStarting");
#endif
        default:
            break;
        }
    }

    int pos = _jack->positionChange();

    if (_nonzeroStart > 0) {
        pos = -1;
        _nonzeroStart--;
        DEVLOG_DEBUG("NONZERO START " + STR(_nonzeroStart));
    }

    if (pos != -1 && _audio->transportPosition() != pos) {
        _audio->setTransportPosition(pos);
        // XXX if(_activeSynth) _activeSynth->stopThread();
        _localRequestStartPlayback = false;
    }

    // FIXME: if(_jack->isRolling() || (_audio->useJackTransport()==false &&
    // _audio->localTransportRolling()))
    // transportPositionChanged(_samplesToMeasures*_audio->transportPosition());
}

void qtauController::outbuf_startPlayback() {
    // startPlayback(0);
    DEVLOG_DEBUG("playback is stable");
}

void qtauController::outbuf_stopPlayback() {
    stopPlayback();
}

qtauController::~qtauController() {
    delete _jack;
    delete _audio;
    delete _mainWindow;
}

//------------------------------------------

static qtauController *singleton = 0;

void qtauController::shutdown(int rc) {
    (void)rc;
    _jack->shutdown();
    _audio->shutdown();  // kind of CopyEngine
}

bool qtauController::run() {
    _mainWindow = new MainWindow();
    _mainWindow->show();

    newEmptySession();
    _mainWindow->setController(*this, *this->_activeSession);

    singleton = this;

    return true;
}

qtauController *qtauController::instance() {
    return singleton;
}

bool qtauController::setupVoicebanks() {
    bool found = false;
    QStringList ret;
    QStringList searchPaths;
    QString userpath = QDir::home().filePath(".local/share/utau/voice");
    searchPaths << userpath;
    if(!QFile(userpath).exists()) {
        QDir::home().mkpath(".local/share/utau/voice");
        //first start -- voices dir is empty
    }
    searchPaths << "/usr/share/utau/voice";
    searchPaths << "/usr/local/share/utau/voice";
    foreach (QString searchPath, searchPaths) {
        QDir dir(searchPath);
        QDirIterator it(dir);
        while (it.hasNext()) {
            QString vdir = it.next();
            QString file = it.fileName();

            if (file == "." || file == "..") continue;

            _voicesMap[file] = vdir;
            DEVLOG_DEBUG(vdir);
            found = true;
        }
    }
    return found;
}

bool qtauController::setupPlugins() {
    // FIXME plugins dir, should not be hardcoded
    _pluginsDir = QDir(qApp->applicationDirPath());

    if (_pluginsDir.cd("plugins") == false) {
        _pluginsDir = QDir(_prefix + "/lib/qtau/");
        if (_pluginsDir.cd("plugins") == false) return false;
    }

    foreach (QString fileName, _pluginsDir.entryList(QDir::Files)) {
        QPluginLoader loader(_pluginsDir.absoluteFilePath(fileName));
        QObject *plugin = loader.instance();

        if (plugin) {
            ISynth *s = qobject_cast<ISynth *>(plugin);

            if (s)
                initSynth(s);
            else {
                IPreviewSynth *ps = qobject_cast<IPreviewSynth *>(plugin);
                if (ps) {
                    initPreviewSynth(ps);
                }
            }

        } else
            DEVLOG_INFO("Incompatible plugin: " + fileName +
                        "reason: " + loader.errorString());
    }

    return false;
}

void qtauController::initPreviewSynth(IPreviewSynth *ps) {
    ps->setup(this);
    _audio->setPreviewSynth(ps);
    _preview = ps;
}

void qtauController::initSynth(ISynth *s) {
    if (!_synths.contains(s->name())) {
        s->setup(this);
        DEVLOG_INFO("Adding synthesizer " + s->name());
        _synths[s->name()] = s;
        _synth = s;
    } else
        DEVLOG_INFO("Synthesizer " + s->name() + " is already registered!");
}

void qtauController::selectSinger(QString singerName) {
    DEVLOG_DEBUG("selectSinger " + singerName);

    foreach (QString synthName, _synths.keys()) {
        _synths[synthName]->setVoicePath(_voicesMap[singerName]);
    }
}

void qtauController::newEmptySession() {
    _activeSession = new qtauSession(this);
}

//------------------------------------------

void qtauController::addFileToRecentFiles(QString fileName) {
    DEVLOG_DEBUG("addFileToRecentFiles: " + fileName);

    QSettings settings("QTau", c_qtau_name);
    QStringList files = settings.value("recentFileList").toStringList();
    files.removeAll(fileName);
    files.prepend(fileName);
    while (files.size() > MAXRECENTFILES) files.removeLast();

    foreach (QString file, files)
        DEVLOG_DEBUG("recent: " + file);

    settings.setValue("recentFileList", files);

    _mainWindow->updateRecentFileActions();
}

void qtauController::onLoadUST(QString fileName) {
    if (!fileName.isEmpty()) {
        if (!_activeSession) newEmptySession();
        _activeSession->loadUST(fileName);
        addFileToRecentFiles(fileName);

    } else
        DEVLOG_WARNING("empty UST file name");
}

void qtauController::onSaveUST(QString fileName, bool rewrite) {
    if (_activeSession && !_activeSession->isSessionEmpty()) {
        QFile uf(fileName);

        if (uf.open(QFile::WriteOnly)) {
            addFileToRecentFiles(fileName);
            if (uf.size() == 0 || rewrite) {
                uf.reset();  // maybe it's redundant?..
                QJsonArray array;
                _activeSession->ustJson(array);
                QJsonDocument doc(array);
                uf.write(doc.toJson());
                uf.close();

                _activeSession->setFilePath(fileName);
                _activeSession->setSaved();

                DEVLOG_DEBUG("UST saved to " + fileName);
            } else
                DEVLOG_ERROR("File " + fileName + " is not empty, rewriting cancelled");
        } else
            DEVLOG_ERROR("Could not open file " + fileName + " to save UST");
    } else
        DEVLOG_ERROR("Trying to save ust from empty session!");
}

// new synth api required (madde)
void qtauController::pianoKeyPressed(int keyNum) {
    _previewRunning = true;
    if (_preview) _preview->start(keyNum);
}

void qtauController::pianoKeyReleased(int keyNum) {
    DEVLOG_DEBUG("piano key released " + STR(keyNum));
    _previewRunning = false;
    if (_preview) _preview->stop();
}

void qtauController::onRequestStartPlayback() {
    _localRequestStartPlayback = true;
    QJsonArray ust;
    _activeSession->ustJson(ust);

    DEVLOG_DEBUG("create score");
    TempoMap *tempoMap = _mainWindow->getTempoMap();
    QtauScore *score = new QtauScore(ust, tempoMap);
    if (score->getNoteCount() == 0) {
        _mainWindow->onLog("Empty session - nothing to do", ELog::error);
        _jack->changeTranportState(TRANSPORT_STOP);
        return;
    }

    if (_synth == nullptr) {
        _mainWindow->onLog("No synth", ELog::error);
        _jack->changeTranportState(TRANSPORT_STOP);
        return;
    }

    if (_voicesMap.size() == 0) {
        _mainWindow->onLog("No voice source", ELog::error);
        _jack->changeTranportState(TRANSPORT_STOP);
        return;
    }

    bool result = _synth->synthesize(score);
    if (result) {
        if (0) {  // synth: [refactor] cache and async transport
            QString tmp = _activeSession->documentFile();
            tmp.replace(".ustj", ".cache");
            if (tmp.length() == 0) tmp = "/tmp";
            _synth->setCacheDir(tmp);
            if (_audio->useJackTransport())
                _jack->changeTranportState(TRANSPORT_STOP);
            else
                _audio->setLocalTransportRolling(false);
        }
        if (_synth->synthIsRealtime()) {
            DEVLOG_DEBUG("schedule synth");
            _outbuf->scheduleSynth(_synth);
            startPlayback(0);
        }
    }
}

void qtauController::onRequestStopPlayback() {
    _localRequestStartPlayback = false;
    if (_audio->useJackTransport())
        _jack->changeTranportState(TRANSPORT_STOP);
    else {
        _audio->setLocalTransportRolling(false);
        // XXX if(_activeSynth) _activeSynth->stopThread();
    }
}

void qtauController::onRequestResetPlayback() {
    _localRequestStartPlayback = false;
    // XXX if(_activeSynth) _activeSynth->stopThread();

    _audio->setTransportPosition(0);
    if (_audio->useJackTransport()) {
        _jack->changeTranportState(TRANSPORT_ZERO);
    }
}

// from synth plugin
void qtauController::startPlayback(float startPos) {
    //_synthrunning = true;
    // _mainWindow->onLog("start playback",ELog::info);
    if (_audio->useJackTransport() == false) {
        _audio->setLocalTransportRolling(true);
        float _sampleRate_ = _jack->sampleRate();  // FIXXME
        if (startPos) _audio->setTransportPosition(_sampleRate_ * startPos);
    } else {
        if (startPos == 0) {
            _nonzeroStart = 0;
            _jack->changeTranportState(TRANSPORT_START);
        } else {
            _nonzeroStart = 30;
            float _sampleRate_ = _jack->sampleRate();
            _audio->setTransportPosition(_sampleRate_ * startPos);
            _jack->transportStartPos(startPos);
        }
    }
}

void qtauController::stopPlayback() {
    _localRequestStartPlayback = false;
    //_synthrunning = false;
    //_mainWindow->onLog("stop playback",ELog::info);
    if (_audio->useJackTransport() == false) {
        _audio->setLocalTransportRolling(false);
        transportPositionChanged(-1);
    } else
        _jack->changeTranportState(TRANSPORT_STOP);
}

void qtauController::setJackTranportEnabled(bool enabled) {
    if (enabled == false) _jack->changeTranportState(TRANSPORT_STOP);

    _audio->setLocalTransportRolling(false);
    _jack->setUseTransport(enabled);
    _audio->setUseJackTransport(enabled);
}

void qtauController::updateTempoTimeSignature(int tempo) {
    (void)tempo;  // FIXME
    //REMOVE
}

void qtauController::logError(const QString &error) {
    _mainWindow->onLog(error, ELog::error);
}

void qtauController::logDebug(const QString &debug) {
    _mainWindow->onLog(debug, ELog::debug);
}

void qtauController::logSuccess(const QString &success) {
    _mainWindow->onLog(success, ELog::success);
}

void qtauController::addPluginAction(QAction *action) {
    _mainWindow->addPluginAction(action);
}

void qtauController::openVoiceDirectory()
{
    QString path = QDir::home().filePath(".local/share/utau/voice");

    QProcess xdg;
    xdg.start("xdg-open", QStringList() << path);
    if (!xdg.waitForStarted())
        return;
    if (!xdg.waitForFinished())
        return;
}

void qtauController::rescanVoiceDirectory()
{
    if(setupVoicebanks())
    {
        emit voiceListChanged();
    }
}
